/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.gwt.remoteaction.server;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import com.google.code.gwt.remoteaction.client.Action;
import com.google.code.gwt.remoteaction.client.RemoteAction;
import com.google.code.gwt.remoteaction.client.RemoteActionService;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import com.google.inject.binder.LinkedBindingBuilder;

/**
 * A Guice module base class for modules that bind action services.  The {@code
 * bindActionService} method should be used to bind the service implementation
 * class to the action service interface.
 *
 * @author jgennis
 */
public abstract class AbstractActionModule extends AbstractModule {

  /**
   * Bind an action service interface to an implementation class.
   *
   * @param <T> the action service interface type
   * @param serviceClass the action service interface class
   * @return a binding builder that can be used to bind the service implementation
   */
  protected final <T extends RemoteActionService> LinkedBindingBuilder<T> bindActionService(
      Class<T> serviceClass) {
    Method[] methods = serviceClass.getMethods();
    for (Method method : methods) {
      RemoteAction remoteAction = method.getAnnotation(RemoteAction.class);
      if (remoteAction == null) {
        super.addError("Remote action service method " + serviceClass.getCanonicalName() + "." +
            method.getName() + " is not annotated with " + RemoteAction.class.getCanonicalName());
        continue;
      }
      Class<? extends Action> actionClass = remoteAction.value();
      if (!isValidMethodAction(method, actionClass)) {
        super.addError("The parameter types of remote action service method " +
            serviceClass.getCanonicalName() + "." + method.getName() + " do not match any of the " +
            "constructors of action class " + actionClass.getCanonicalName());
      } else {
        bindActionMethod(serviceClass, method, actionClass);
      }
    }
    return super.bind(serviceClass).annotatedWith(ActionService.class);
  }

  /**
   * Perform the bindings that are used to execute a given action method.  The
   * bindings use the {@link ActionHandler} annotation to indicate that they
   * are used for handling the given action class.
   * 
   * @param method the method implementing the action
   * @param actionClass the action class
   */
  void bindActionMethod(Class<? extends RemoteActionService> serviceClass, Method method,
      Class<? extends Action> actionClass) {
    Key<? extends RemoteActionService> serviceKey = Key.get(serviceClass, ActionService.class);
    ActionHandler annotation = new ActionHandlerImpl(actionClass);
    ActionInvoker actionInvoker;
    try {
      actionInvoker = new ActionInvoker(serviceClass, method, actionClass);
    } catch (NoSuchActionPropertyException ex) {
      super.addError(ex);
      return;
    } catch (AmbiguousParameterException ex) {
      super.addError(ex);
      return;
    }
    ActionResultFactory actionResultFactory = createActionResultFactory(method, actionClass);

    // Bind everything that we'll need to execute this method later when we get
    // the action class.
    super.bind(RemoteActionService.class).annotatedWith(annotation).to(serviceKey);
    super.bind(ActionInvoker.class).annotatedWith(annotation).toInstance(actionInvoker);
    super.bind(ActionResultFactory.class).annotatedWith(annotation).toInstance(
        actionResultFactory);
  }

  /**
   * Create an ActionResultFactory instance for handling the result of a given
   * method action.
   *
   * @param method the method implementing the action
   * @param actionClass the action class
   * @return a new ActionResultFactory instance for creating ActionResult
   *         objects for the result of executing {@code method}
   */
  @SuppressWarnings("unchecked")
  static ActionResultFactory createActionResultFactory(Method method,
      Class<? extends Action> actionClass) {
    String className = Utils.getResultCanonicalClassName(actionClass.getCanonicalName());
    Class<?> resultType = method.getReturnType();
    Set<Class<? extends Throwable>> throwableTypes =
        new HashSet(Arrays.asList(method.getExceptionTypes()));
    return new ActionResultFactory(className, resultType, throwableTypes);
  }

  /**
   * Check that the given action class has a constructor that matches the given
   * method's parameter types.
   * 
   * @param method the method
   * @param actionClass the action class
   */
  static boolean isValidMethodAction(Method method, Class<? extends Action> actionClass) {
    for (Constructor<?> constructor : actionClass.getConstructors()) {
      if (doParameterTypesMatch(method, constructor))
        return true;
    }
    return false;
  }

  /**
   * Determine whether the parameter types match between a given method and
   * constructor.
   * 
   * @param method the method
   * @param constructor the constructor
   * @return {@code true} if the parameter types match, {@code false}
   *         otherwise.
   */
  @SuppressWarnings("unchecked")
  static boolean doParameterTypesMatch(Method method, Constructor constructor) {
    Class[] methodParameterTypes = method.getParameterTypes();
    Class[] constructorParameterTypes = constructor.getParameterTypes();
    if (methodParameterTypes.length != constructorParameterTypes.length)
      return false;
    for (int i = 0; i < methodParameterTypes.length; i++) {
      if (!methodParameterTypes[i].equals(constructorParameterTypes[i]))
        return false;
    }
    return true;
  }
}
